package com.advert.service.util;

import com.advert.dto.image.ImageScalingResult;
import com.advert.dto.image.ScalingDimensions;
import com.advert.model.advert.AdvertEntity;
import com.advert.model.advert.ImageBase;
import com.advert.model.advert.ImageEntity;
import com.advert.model.advert.ImageLobsEntity;
import com.advert.model.security.AccessMode;
import com.advert.model.security.Status;
import com.advert.provider.util.ImageProcessingProvider;
import com.advert.security.AppSecurityManager;
import com.advert.service.admin.AdminService;
import com.advert.service.advert.AdvertService;
import com.advert.util.*;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.*;
import org.jboss.seam.core.Events;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;


@Name("ImageProcessingService")
@Scope(ScopeType.APPLICATION)
@AutoCreate
public class ImageProcessingService {
    @In("ImageProcessingProvider")
    private ImageProcessingProvider imageProcessingProvider;
    @In("executorService")
    private ExecutorService executorService;
    @In("SecurityManager")
    private AppSecurityManager securityManager;
    @In("AdvertService")
    private AdvertService advertService;
    @In("AdminService")
    private AdminService adminService;    


    /**
     * Scales images and store result images in the same
     * directory with "scaled" and "preview" suffixes
     *
     * @param initImageFile - initial image file
     * @param dimensions - required dimensions of scled images
     * @return future for scaling task
     */
    public Future<ImageScalingResult> scaleImage(File initImageFile, ScalingDimensions dimensions) {
        File scaledImageFile = new File(initImageFile.getAbsolutePath() + "scaled");
        File previewImageFile = new File(initImageFile.getAbsolutePath() + "preview");
        return executorService.submit(new ImageScalingCallable(initImageFile, scaledImageFile, previewImageFile, dimensions));
    }

    /**
     * Moves scaled images to web accessed directory specified by context parameter.
     * Deletes temporary files.
     *
     * @param scalingResult - result of image scaling
     * @param imageId - database id for stored image
     * @param directory - directory to store image files
     * @return true is success
     * @throws IOException
     */
    public boolean storeImages(ImageScalingResult scalingResult, Integer imageId, File directory) throws IOException {
        boolean retVal = false;
        File dest = new File(directory.getAbsolutePath() + "/" + imageId + ".jpg");
        copyFile(scalingResult.getScaledImageFile(), dest);
        File destPreview = new File(directory.getAbsolutePath() + "/" + imageId + "_preview.jpg");
        copyFile(scalingResult.getPreviewImageFile(), destPreview);
        retVal = scalingResult.getScaledImageFile().delete();
        retVal = retVal & scalingResult.getInitImageFile().delete();
        retVal = retVal & scalingResult.getPreviewImageFile().delete();
        
        return retVal;
    }

    public void removeImages(ImageScalingResult scalingResult) {
        if (scalingResult.getInitImageFile() != null) {
            scalingResult.getInitImageFile().delete();
        }
        if (scalingResult.getPreviewImageFile() != null) {
            scalingResult.getPreviewImageFile().delete();
        }
        if (scalingResult.getScaledImageFile() != null) {
            scalingResult.getScaledImageFile().delete();
        }
    }

    /**
     * Loads image and preview with given id from database and store them
     * to web accessed directory specified by context parameter
     *
     * @param imageId - image id
     * @param directory - directory to store image files
     * @throws IOException
     */
    public void cacheImagesOnDisk(Integer imageId, File directory) throws IOException {
        if (imageId != null) {
            ImageLobsEntity lobs = imageProcessingProvider.getImageLobs(imageId);
            if (lobs != null) {
                if (!directory.exists()) {
                    directory.mkdir();
                }
                File image = new File(directory.getAbsolutePath() + "/" + imageId + ".jpg");
                File imagePreview = new File(directory.getAbsolutePath() + "/" + imageId + "_preview.jpg");

                if (image.createNewFile()) {
                    FileOutputStream fos = new FileOutputStream(image);
                    fos.write(lobs.getImage());
                    fos.flush();
                    fos.close();
                }
                if (imagePreview.createNewFile()) {
                    FileOutputStream fos = new FileOutputStream(imagePreview);
                    fos.write(lobs.getPreview());
                    fos.flush();
                    fos.close();
                }
            }
        }
    }

    /**
     * Removes image from database and from web accessed directory
     *
     * @param image - image
     * @param directory - directory to store image files
     */
    @Transactional
    public void removeImage(ImageEntity image, File directory) {
        imageProcessingProvider.removeImage(image);
        
        File img = new File(directory.getAbsolutePath() + "/" + image.getId() + ".jpg");
        File imgPreview = new File(directory.getAbsolutePath() + "/" + image.getId() + "_preview.jpg");
        if (img.exists()) {
            img.delete();
        }
        if (imgPreview.exists()) {
            imgPreview.delete();
        }
    }

    /**
     * Creates image in database and store it in web accessed directory
     *
     * @param scalingResult - result of image scaling
     * @param directory - directory to store image files
     * @return
     * @throws IOException
     */
    @Transactional
    public ImageBase createImage(ImageScalingResult scalingResult, AdvertEntity advert, File directory) throws IOException {
        ImageBase retVal = buildImageLobs(scalingResult);
        retVal.setAdvert(advert);
        retVal = imageProcessingProvider.createImage(retVal);
        advert.setStatus(Status.PENDING);
        advertService.updateAdvert(advert);
        storeImages(scalingResult, retVal.getId(), directory);
        adminService.notifyAboutPendingAdvert();
        return retVal;
    }

    public ImageLobsEntity buildImageLobs(ImageScalingResult scalingResult) throws IOException {
        Date today = new Date();
        ImageLobsEntity image = new ImageLobsEntity();

        image.setCreateDate(today);
        image.setUpdateDate(today);
        image.setOwnerId(securityManager.userId());

        FileInputStream fis = new FileInputStream(scalingResult.getScaledImageFile());
        byte[] imageBytes = new byte[fis.available()];
        fis.read(imageBytes, 0, fis.available());
        fis.close();
        image.setImage(imageBytes);

        fis = new FileInputStream(scalingResult.getPreviewImageFile());
        imageBytes = new byte[fis.available()];
        fis.read(imageBytes, 0, fis.available());
        fis.close();
        image.setPreview(imageBytes);

        return image;
    }


    private void copyFile(File source, File dest) throws IOException {
        FileChannel in = null, out = null;
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream(source);
            fos = new FileOutputStream(dest);
            in = fis.getChannel();
            out = fos.getChannel();

            long size = in.size();
            out.transferFrom(in, 0, size);
        } finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
            if (fis != null) {
                fis.close();
            }
            if (fos != null) {
                fos.close();
            }
        }
    }

    private class ImageScalingCallable implements Callable<ImageScalingResult> {
        private File oldImageFile;
        private File newImageFile;
        private File previewImageFile;
        private ScalingDimensions dimensions;

        private ImageScalingCallable(File oldImageFile, File newImageFile, File previewImageFile, ScalingDimensions dimensions) {
            this.oldImageFile = oldImageFile;
            this.newImageFile = newImageFile;
            this.previewImageFile = previewImageFile;
            this.dimensions = dimensions;
        }

        public ImageScalingResult call() throws Exception {
            Image image = ImageIO.read(oldImageFile);
            scale(image, newImageFile, dimensions.getWidth(), dimensions.getHeight());
            scale(image, previewImageFile, dimensions.getPreviewWidth(), dimensions.getPreviewHeight());
            return new ImageScalingResult(oldImageFile, newImageFile, previewImageFile);
        }

        private void scale(Image image, File dest, int width, int height) throws Exception {
            int newHeight = calculateHeight(width, ((BufferedImage)image).getHeight(), ((BufferedImage)image).getWidth());
            int newWidth = calculateWidth(height, ((BufferedImage)image).getHeight(), ((BufferedImage)image).getWidth());
            if (newHeight > height) {
                newHeight = calculateHeight(newWidth, ((BufferedImage)image).getHeight(), ((BufferedImage)image).getWidth());
            }
            if (newWidth > width) {
                newWidth = calculateWidth(newHeight, ((BufferedImage)image).getHeight(), ((BufferedImage)image).getWidth());
            }
            Image newImg = image.getScaledInstance(newWidth, newHeight, Image.SCALE_AREA_AVERAGING);
            BufferedImage bim = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
            bim.createGraphics().drawImage(newImg, 0, 0, null);
            FileOutputStream fos = new FileOutputStream(dest);
            ImageIO.write(bim, "jpg", fos);
            fos.close();
        }

        private int calculateHeight(int newWidth, int initHeight, int initWidth) {
            float ratio = (float)initWidth / (float)initHeight;
            return Math.round(newWidth / ratio);
        }

        private int calculateWidth(int newHeight, int initHeight, int initWidth) {
            float ratio = (float)initWidth / (float)initHeight;
            return Math.round(newHeight * ratio);
        }
    }
}
